﻿using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Ooaavee.TestTools
{
    /// <summary>
    /// Base class for unit tests.
    /// </summary>
    [TestClass]
    public abstract class TestBase
    {

        #region Fields

        /// <summary>
        /// Test hooks will be created and disposed in this order.
        /// </summary>
        private static readonly TestHookCategory[] CreateAndDisposeOrder = new[]
                                                                               {
                                                                                   TestHookCategory.RunBeforeTest,
                                                                                   TestHookCategory.RunAfterTest,
                                                                                   TestHookCategory.RunAftedFailedTest,
                                                                                   TestHookCategory.RunAfterPassedTest
                                                                               };

        private readonly IDictionary<TestHookCategory, IList<TestHookHandle>> _handles = new Dictionary<TestHookCategory, IList<TestHookHandle>>();

        #endregion

        /// <summary>
        /// Occurs before the test method.
        /// </summary>
        public event EventHandler<TestMethodEventArgs> BeforeTest;
        
        /// <summary>
        /// Occurs after the test method.
        /// </summary>
        public event EventHandler<TestMethodEventArgs> AfterTest;
        
        /// <summary>
        /// Occurs after the test method, if the test method was passed.
        /// </summary>
        public event EventHandler<TestMethodEventArgs> AfterPassedTest;
        
        /// <summary>
        /// Occurs after the test method, if the test method was failed.
        /// </summary>
        public event EventHandler<TestMethodEventArgs> AfterFailedTest;

        /// <summary>
        /// Gets or sets the test context which provides information about and functionality for the current test run.
        /// </summary>
        public TestContext TestContext { get; set; }

        /// <summary>
        /// Gets the test hooks context which provides information about and functionality for the current test run.
        /// </summary>
        public ExtendedTestContext ExtendedTestContext { private set; get; }

        /// <summary>
        /// Do not call this method in the user code!
        /// </summary>
        /// <remarks>
        /// This method will be run before each test method.
        /// </remarks>
        [TestInitialize]
        public void TestInitialize()
        {
            ExtendedTestContext = new ExtendedTestContext(this);

            CreateTestHooks();

            OnBeforeTest(new TestMethodEventArgs(ExtendedTestContext));
        }

        /// <summary>
        /// Do not call this method in the user code!
        /// </summary>
        /// <remarks>
        /// This method will be run after each test method.
        /// </remarks>
        [TestCleanup]
        public void TestCleanup()
        {
            try
            {
                OnAfterTest(new TestMethodEventArgs(ExtendedTestContext));

                switch (TestContext.CurrentTestOutcome)
                {
                    case UnitTestOutcome.Passed:
                        OnAfterPassedTest(new TestMethodEventArgs(ExtendedTestContext));
                        break;
                    case UnitTestOutcome.Failed:
                        OnAfterFailedTest(new TestMethodEventArgs(ExtendedTestContext));
                        break;
                }
            }
            finally
            {
                DisposeTestHooks();
            }
        }

        /// <summary>
        /// Raises the <see cref="BeforeTest"/> event.
        /// </summary>
        /// <param name="e">A <see cref="TestMethodEventArgs"/>  that contains the event data.</param>
        protected virtual void OnBeforeTest(TestMethodEventArgs e)
        {
            RunTestHooks(TestHookCategory.RunBeforeTest);

            if (BeforeTest != null)
            {
                BeforeTest(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="AfterTest"/> event.
        /// </summary>
        /// <param name="e">A <see cref="TestMethodEventArgs"/>  that contains the event data.</param>
        protected virtual void OnAfterTest(TestMethodEventArgs e)
        {
            RunTestHooks(TestHookCategory.RunAfterTest);

            if (AfterTest != null)
            {
                AfterTest(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="AfterPassedTest"/> event.
        /// </summary>
        /// <param name="e">A <see cref="TestMethodEventArgs"/>  that contains the event data.</param>
        protected virtual void OnAfterPassedTest(TestMethodEventArgs e)
        {
            RunTestHooks(TestHookCategory.RunAfterPassedTest);

            if (AfterPassedTest != null)
            {
                AfterPassedTest(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="AfterFailedTest"/> event.
        /// </summary>
        /// <param name="e">A <see cref="TestMethodEventArgs"/>  that contains the event data.</param>
        protected virtual void OnAfterFailedTest(TestMethodEventArgs e)
        {
            RunTestHooks(TestHookCategory.RunAftedFailedTest);

            if (AfterFailedTest != null)
            {
                AfterFailedTest(this, e);
            }
        }

        /// <summary>
        /// Creates test hooks for the current test method.
        /// </summary>
        private void CreateTestHooks()
        {
            foreach (TestHookCategory category in CreateAndDisposeOrder)
            {
                _handles.Add(category, new List<TestHookHandle>());

                TestHookFactory.CreateTestHooks(ExtendedTestContext.TestMethod,
                                                category, ExtendedTestContext,
                                                hook => _handles[category].Add(hook));
            }
        }

        /// <summary>
        /// Disposes all test hooks of the current test method.
        /// </summary>
        private void DisposeTestHooks()
        {
            foreach (TestHookCategory category in CreateAndDisposeOrder)
            {
                IList<TestHookHandle> handles;
                if (_handles.TryGetValue(category, out handles))
                {
                    foreach (TestHookHandle handle in handles)
                    {
                        ((IDisposable) handle).Dispose();
                    }
                }
            }
        }

        /// <summary>
        /// Runs test hooks of the current test method.
        /// </summary>
        /// <param name="category">Defines the category of test hooks that should be run.</param>
        private void RunTestHooks(TestHookCategory category)
        {
            IList<TestHookHandle> handles;
            if (_handles.TryGetValue(category, out handles))
            {
                foreach (TestHookHandle handle in handles)
                {
                    handle.Run();
                }
            }
        }
    }
}